查看原文
其他

看雪·深信服 2021 KCTF 春季赛 | 第五题设计思路及解析

KCTF 看雪学院 2021-05-18

武林大会在即,看雪er们跟随武林少才的脚步齐聚华山,开启第五题《华山论剑》,一共有 支战队完成目标。



xtgo战队当仁不让,率先拿下本题,仅用时52092秒。 hzqmwne战队雨落星沉战队紧追不舍,很快也成功攻破此题。



目前赛程已经过半,我们来看看目前场上的状况吧!攻击方排名前10如下:



大家在赛场上你追我赶,好不热闹!接下来我们一起来看看本题的“通关宝典”吧!



出题团队简介


本题出题战队为 ArmVMP :



战队队长 AJISky就职于梆梆安全多年,热爱阅读与跑步,痴迷于二进制技术研究。
主攻方向基于二进制方案的文件及虚拟机保护,覆盖行业包括安卓,ios平台,及物联网平台,致力于通过技术手段,以减少对用户的恶意攻击及破坏行为。

专家点评


看雪专家netwind点评:该题在反调试方面采用了指令虚拟化的方式来干扰攻击方进行代码逻辑分析,在算法方面采用改动过的SHA1算法和RC4算法来保护序列号;攻击方需要在识别出指令虚拟化逻辑以及加密算法的修改逻辑的基础上才能很好的完成此题;作为安卓平台的题目,难度中等偏上,能够完成此题证明攻防双方都具备较强的实力!


赛题设计思路


本题目是安卓平台的crackme。
算法简单,可玩性高,有兴趣可以随时交流。

规则2的demo为:KCTF-2.sign.apk


其中规则2的两组序列号如下:

* name:ed8b9244350d3644

serial:7C9815255BFE832D3F93140B


* name:KCTF

serial:17726331DA0fE737149c8202



设计思路




1. 对输入name字符串通过SHA1算法(稍微改动)计算得到24字节的hash值。
2. hash值做明文和Java_com_example_hellojni_HelloJni_stringFromJNI地址做密钥K参与rc4运算(稍作修改),其中hash前12字节与密钥流按位做异或并与hash后12字节按位相加得到新的12字节串值。

3. 将新得到的十六进制12字节串转换为ASCII码形式即为24字节的serial值。

 

保护方法




通过汇编实现了一套模拟add/sub/eor/push/pop等指令的函数块,对so的函数指令流做分析,需要保护的指令做指令跳转替换后生成位置无关的shellcode,并patch到so的末尾生成新so文件,原函数的头部替换成跳转指令跳转到patch位置执行,被保护后的add/sub...等指令,会以跳转指令的形式跳到对应的模拟函数执行。
 

解题思路




由于demo编译后通过,对代码段的指令做了变形保护,通过相同功能的函数实现指令替代运行,所以反编译时,不容易看出算法逻辑,需要对模拟代码标识出相应的指令来分析逻辑,当然也存在不需要标记的情况,只需要找到关键做异或之处即可:首先在文件偏移0x6448下断点,在0x76A8就可以跟踪出serial的结果。



赛题解析


本赛题解析由看雪论坛 mb_mgodlfyn 给出:


用jadx打开,发现java层只有简单的输入输出,检查逻辑在native层。 解包apk,ida打开libhello-jni.so看native层,代码很乱,考虑动态调试。
先尝试用ida在真机上调试,体验很不好(最主要的问题是,0x5000处的汇编指令ida识别为"BL LR, #0xBA ",但是单步调试时无法进入这条指令内部,不知道原因)
 考虑到这个so文件接口不复杂,于是写了一个简单的loader在linux上直接加载(按相对偏移mmap所有的load segment;自己定义一些dummy函数填入got表和JNIEnv.functions表),然后gdb本地调试,体验非常好(方便随时重启;配合插件能看多级指针)(另:0x5000处的汇编在gdb里识别为"ldr lr, [sp], #4",和ida里不一样,而且能单步调试)(但是有一个坑,不能下数据断点(rwatch/watch),让后续分析变得麻烦) loader的代码:
#include <stdio.h>#include <string.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <sys/mman.h>#include <assert.h> struct str { const char *s; int len;}; //struct str global_name = {.s = "ed8b9244350d3644", .len = 16};//struct str global_serial = {.s = "7C9815255BFE832D3F93140B", .len = 24}; struct str global_name = {.s = "KCTF", .len = 4};struct str global_serial = {.s = "17726331DA0FE737149C8202", .len = 24};//struct str global_serial = {.s = "17726331da0fe737149c8202", .len = 24}; struct JNINativeInterface_ { unsigned int f[0x1000/4];}; typedef struct JNIEnv_ { struct JNINativeInterface_ *functions; } JNIEnv; void JNICALL_FindClass(JNIEnv *env, const char *name) { printf("JNICALL_FindClass %s\n", name);} void JNICALL_NewStringUTF(JNIEnv *env, const char *utf) { printf("%s %s\n", __func__, utf);} int JNICALL_GetMethodID(JNIEnv *env, void *clazz, const char *name, const char *sig) { printf("%s %p %s %s\n", __func__, clazz, name, sig); return 0x55555501;} void *JNICALL_CallObjectMethod(JNIEnv *env, void *obj, int methodID) { assert(methodID == 0x55555501); printf("%s %p %x\n", __func__, obj, methodID); return obj;} int JNICALL_GetArrayLength(JNIEnv *env, struct str *array) { printf("%s %p\n", __func__, array); return array->len;} unsigned char *JNICALL_GetByteArrayElements(JNIEnv *env, struct str *array, int isCopy) { printf("%s %p %d\n", __func__, array, isCopy); return array->s;} void JNICALL_ReleaseByteArrayElements(JNIEnv *env, void *array, void *elems, int mode) { printf("%s %p %p %d\n", __func__, array, elems, mode);} void *got_malloc(int size) { void *r = malloc(size); printf("%s %d %p\n", __func__, size, r); return r;} void got_free(void *p) { printf("%s\n", __func__); free(p);} void got_memset(char *p, int n, int count) { printf("%s %p %d %d\n", __func__, p, n, count); memset(p, n, count);} void bp(void) { ;} void stack_chk_guard(void) { printf("%s\n", __func__);} void imp___gnu_Unwind_Find_exidx(void) { printf("%s\n", __func__);} void cxa_call_unexpected(void) { printf("%s\n", __func__);} int main(void) { int fd = open("libhello-jni.so", O_RDONLY); unsigned char *fmem = mmap(NULL, 0x7000, PROT_READ, MAP_PRIVATE, fd, 0); unsigned char *mem = mmap(0xdead0000, 0x8000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); memcpy(mem, fmem, 0x2b13); memcpy(mem+0x3e8c, fmem+0x2e8c, 0x1a8); memcpy(mem+0x5000, fmem+0x4000, 0x26bc); munmap(fmem, 0x7000); close(fd); *(unsigned int *)(mem+0x3f98) = mem+0x1034+1; // xxxxxxxxxx2+1 *(unsigned int *)(mem+0x3f9c) = stack_chk_guard; *(unsigned int *)(mem+0x3fb8) = imp___gnu_Unwind_Find_exidx; *(unsigned int *)(mem+0x3fc4) = cxa_call_unexpected; *(unsigned int *)(mem+0x3fc8) = mem+0x3f98; // _GLOBAL_OFFSET_TABLE_@got *(unsigned int *)(mem+0x3fa0) = mem+0x10e4+1; // Java_com_example_hellojni_HelloJni_stringFromJNI_ptr+1 *(unsigned int *)(mem+0x3fa4) = mem+0x4004; // f_data_key_dllink *(unsigned int *)(mem+0x3fa8) = mem+0x401c; // f_sucess *(unsigned int *)(mem+0x3fdc) = got_malloc; // malloc@got *(unsigned int *)(mem+0x3fe0) = got_memset; // memset@got *(unsigned int *)(mem+0x3fe4) = got_free; // free@got for (int i = 0x3f98; i < 0x4000; i+=4) { if (*(unsigned int *)(mem+i) == 0) { //*(unsigned int *)(0x77770000+i) = i; } } struct JNINativeInterface_ jnii = {.f = {0}}; for(int i = 0; i < 0x100; i++) { jnii.f[i] = 0x11110000+i*4; } jnii.f[0x18/4] = JNICALL_FindClass; jnii.f[0x29c/4] = JNICALL_NewStringUTF; jnii.f[0x84/4] = JNICALL_GetMethodID; jnii.f[0x88/4] = JNICALL_CallObjectMethod; jnii.f[0x2ac/4] = JNICALL_GetArrayLength; jnii.f[0x2e0/4] = JNICALL_GetByteArrayElements; jnii.f[0x300/4] = JNICALL_ReleaseByteArrayElements; JNIEnv env; env.functions = &jnii; printf("&global_name: %p, &global_serial: %p\n", &global_name, &global_serial); bp(); ((void (*)(int, int, int, int, int))(mem+0x10e4+1))(&env, 0xaaaa, &global_name, &global_serial, 0xdddd); return 0;}
编译:arm-linux-gnueabi-gcc loader.c -g -mthumb -o a.out
运行:qemu-arm -L /usr/arm-linux-gnueabi/ -g 1234 ./a.out
调试:
gdb-multiarch -ex "file ./a.out" -ex "target remote localhost:1234" (在Ubuntu上可以apt-get install gcc-arm-linux-gnueabi libc6-armel-cross gdb-multiarch 安装依赖) 题目是类似vmp的虚拟机。 虚拟机指令的结构:以0x5d04处为例:
从thumb指令开始(0x5d04),通过一个B跳转跳过1或2个dword(0x5d06和0x5d08)到后面的arm指令(0x5d10),先是 BX PC ,然后是 STR PC, [SP,#var_FC] 把PC放入栈,最后B跳转到一个外部函数(0x7270),外部函数返回到下一条指令。

外部函数大部分以push所有寄存器开始,以pop所有寄存器结束,通过栈上保存的PC向前找参数(0x5d06和0x5d08),返回到下一条指令(0x5d1c)。
LOAD:00005D04 ; ---------------------------------------------------------------------------LOAD:00005D04 CODE16LOAD:00005D04 B sub_5D10 ; BranchLOAD:00005D04 ; ---------------------------------------------------------------------------LOAD:00005D06 CODE32LOAD:00005D06 DCW 0xBF00LOAD:00005D08 DCD 8, 0x30D00LOAD:00005D10 CODE16LOAD:00005D10LOAD:00005D10 ; =============== S U B R O U T I N E =======================================LOAD:00005D10LOAD:00005D10 ; Attributes: thunkLOAD:00005D10LOAD:00005D10 sub_5D10 ; CODE XREF: LOAD:00005D04↑jLOAD:00005D10 BX PC ; Branch to/from Thumb modeLOAD:00005D10 ; ---------------------------------------------------------------------------LOAD:00005D12 DCB 1LOAD:00005D13 DCB 0LOAD:00005D13 ; End of function sub_5D10LOAD:00005D13LOAD:00005D14 CODE32LOAD:00005D14LOAD:00005D14 ; =============== S U B R O U T I N E =======================================LOAD:00005D14LOAD:00005D14LOAD:00005D14 sub_5D14 ; CODE XREF: sub_5D10↑jLOAD:00005D14LOAD:00005D14 var_FC = -0xFCLOAD:00005D14LOAD:00005D14 STR PC, [SP,#var_FC] ; Store to MemoryLOAD:00005D18 B sub_7270 ; BranchLOAD:00005D18 ; End of function sub_5D14 LOAD:00005D1C ; ---------------------------------------------------------------------------LOAD:00005D1C CODE16LOAD:00005D1C B sub_5D28 ; BranchLOAD:00005D1C ; ---------------------------------------------------------------------------LOAD:00005D1E CODE32

几个关键的位置:

* 所有的内存读取:0x72fc、0x7304、0x730c* 所有的内存写入:0x71f4、0x71fc、0x7204* 比较(cmp):0x75ac* 加法:0x7644在这8个位置下断点,基本上就能看出程序的完整流程,不需要分析虚拟机指令:* 分配并初始化若干个缓冲区* 对name做一些运算* 对serial做hexdecode(逐字符调用程序里的 sub_DDA 函数,这个函数没有混淆)* 初始化RC4的sbox(0x6054附近,循环)* 计算RC4(0x638c的附近,循环) 

前期调试分析过程很漫长,但最终找出serial很简单:

先在 0x638c 下断点,运行到这里后在 0x71f4 下断点,可以发现每计算出一个加密值,就会存入另一个缓冲区中,且这个值与serial做hexdecode之后的值是相等的。
所以,只需要把name初始化为"KCTF",就可以在这里提取出正确的serial。 name:KCTF
serial:17726331DA0FE737149C8202
 hexdecode没有区分大小写,因此serial的字母大小写可以替换,造成多解。

往期解析


1. 看雪·深信服 2021 KCTF 春季赛 | 第二题设计思路及解析

2. 看雪·深信服 2021 KCTF 春季赛 | 第三题设计思路及解析3. 看雪·深信服 2021 KCTF 春季赛 | 第三题设计思路及解析4. 看雪·深信服 2021 KCTF 春季赛 | 第四题设计思路及解析




主办方


看雪CTF(简称KCTF)是圈内知名度最高的技术竞技之一,从原CrackMe攻防大赛中发展而来,采取线上PK的方式,规则设置严格周全,题目涵盖Windows、Android、iOS、Pwn、智能设备、Web等众多领域。
看雪CTF比赛历史悠久、影响广泛。自2007年以来,看雪已经举办十多个比赛,与包括金山、360、腾讯、阿里等在内的各大公司共同合作举办赛事。比赛吸引了国内一大批安全人士的广泛关注,历年来CTF中人才辈出,汇聚了来自国内众多安全人才,高手对决,精彩异常,成为安全圈的一次比赛盛宴,突出了看雪论坛复合型人才多的优势,成为企业挑选人才的重要途径,在社会安全事业发展中产生了巨大的影响力。

合作伙伴

深信服科技股份有限公司成立于2000年,是一家专注于企业级安全、云计算及基础架构的产品和服务供应商,致力于让用户的IT更简单、更安全、更有价值。目前深信服在全球设有50余个分支机构,员工规模超过7000名。

第六题正在火热进行中,

👆还在等什么,快来参赛吧!




- End -



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com




球分享

球点赞

球在看



戳“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存